##########################################################################################
# "Zenodotus" - Bibliography manager
#  http://code.google.com/p/zenodotus/
#
# Copyright 2010 Stephen Fendyke, Edd Barrett
#
#
# 29th April 2010
#
# Bugs:
#
#   Requires comments to be on a line started with # or !, or be blank (i.e. just contain 
#      a carriage return.
#   
# Features:
#    Can search entries
#    Will handle a bad filename without crashing
#    Can manually add entries
# 
# 2010-05-16 Added HTML export and persistent mainDB picking and unpickling at startup
#            HTML looks okay with normal articles, but a bit silly with books and URL
#            Can delete entries using quickname
#            Can have "projects"
#
# 2010-05-17 Primitive GUI added to display a "table" of the main library. Much work to do here
#            All field names now lowercase
#
# 2010-05-19 Can display search results graphically
#            Can display contents of current project graphically
#
#
# To add: Error checking for all the project operations!
#         Counter for number of entries in a database
#         >> INTERNET LOOKUP <<
#         Edit an existing entry
#         GUI!
#         Export Database as BibTeX
#         Categories for entries, to list them more intelligently
#
##########################################################################################
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##########################################################################################

# Requires the "pickle" module
import pickle
from Tkinter import *

# Subroutine to graphically list the contents of a list of dictionaries. Still
# needs to have buttons and such added to it. This uses Tkinter.
def glist(lib):
            ## Main window - within it, we're going to create scrollbars
            root = Tk()
            root.title("Zenodotus")
            root.geometry("%dx%d%+d%+d" % (1000, 600, 0, 0))
            ## Grid sizing behavior in window
            root.grid_rowconfigure(0, weight=1)
            root.grid_columnconfigure(0, weight=1)
            ## Canvas
            canvas = Canvas(root)
            canvas.grid(row=0, column=0, sticky='nswe')
            ## Scrollbars for canvas
            hScroll = Scrollbar(root, orient=HORIZONTAL, command=canvas.xview)
            hScroll.grid(row=1, column=0, sticky='we')
            vScroll = Scrollbar(root, orient=VERTICAL, command=canvas.yview)
            vScroll.grid(row=0, column=1, sticky='ns')
            canvas.configure(xscrollcommand=hScroll.set, yscrollcommand=vScroll.set)
            ## Frame in canvas
            frm = Frame(canvas)
            ## This puts the frame in the canvas's scrollable zone
            canvas.create_window(0, 0, window=frm, anchor='nw')

            headings = []

            for j in range(0,len(lib)):   # for each dictionary
                for i in lib[j].keys():   # for each key in dictionary
                    if i in headings:
                        pass
                    else:
                        headings.append(i)   

            # Now, we have a list of all headings, with no duplicates
            for i in range(0, len(lib)):
                be = Button(frm, text='Edit', command = editPress, width=5).grid(row=i+1, column=0)

            for i in range(0,len(headings)):     # One column per heading   
                Label(frm,text=headings[i], relief=RIDGE,  width=25).grid(row=0, column=i+1) # Top of column is title
                jcount =0
            for i in range(0, len(lib)):
                for j in lib[i]:            # One row per lib entry
                    #e = Entry(frm,relief=RIDGE)
                    #jcount needs to be such that headings[jcount]=j
                    jcount = headings.index(j)
                    #e.grid(row=i+1, column=jcount, sticky=NSEW)
                    if j in lib[i]:
                        e = Label(frm, text=lib[i][j], relief=RIDGE, bg="white", width=25, wraplength=130).grid(row=i+1, column=jcount+1, sticky = NSEW)
                    
                        #e.insert(END, '%s' % lib[i][j])
            
            ## Update display to get correct dimensions
            frm.update_idletasks()
            ## Configure size of canvas's scrollable zone
            canvas.configure(scrollregion=(0, 0, frm.winfo_width(), frm.winfo_height()))

def editPress():
    # Eventually, will bring up a new window to edit the individual entry. 
    # There will be an option to delete the entry or change any parameters and save them back. 
    print 'Pressed Edit Button'

def MainMenu():
            ## Main window 
            root = Tk()
            root.title("Zenodotus")
            root.geometry("%dx%d%+d%+d" % (200, 600, 0, 0))
            root.resizable(width=FALSE, height=FALSE) # No resizing the main window!
            ## Grid sizing behavior in window
            root.grid_rowconfigure(0, weight=1)
            root.grid_columnconfigure(0, weight=1)
            ## Canvas
            canvas = Canvas(root)
            canvas.grid(row=0, column=0, sticky='nswe')
            ## Frame in canvas
            frm = Frame(canvas)
            ## This puts the frame in the canvas's scrollable zone
            canvas.create_window(0, 0, window=frm, anchor='nw')

            about = Button(frm, text='About Zenodotus', command = aboutPress, width=20).grid(row=0)
            view = Button(frm, text='View Database', command = viewPress, width=20).grid(row=1)
            search = Button(frm, text='Search Database', command = searchDialog, width=20).grid(row=2)            
            search = Button(frm, text='Quit', command = exit, width=20).grid(row=10) 

            ## Update display to get correct dimensions
            frm.update_idletasks()
            ## Configure size of canvas's scrollable zone
            canvas.configure(scrollregion=(0, 0, frm.winfo_width(), frm.winfo_height()))

def aboutPress():
    # Main Menu "About" button pressed
    root = Tk()
    root.title("About")
    w = Label(root, text="Zenodotus\n Bibliography Manager\n (C) 2010")
    w.pack()


def viewPress():
    # Main Menu "View" button pressed
    glist(DB.mainlist)

def searchDialog():
    # Main Menu "Search" button pressed
    # Need to bring up a dialog window with drop down list containing all search fields
    # plus a text box to enter search term
    # Then an okay button
    root = Tk()
    root.title("Search Database")
    root.geometry("%dx%d%+d%+d" % (400, 100, 0, 0))
    root.resizable(width=FALSE, height=FALSE) # No resizing the main window!

    ## Grid sizing behavior in window
    root.grid_rowconfigure(0, weight=1)
    root.grid_columnconfigure(0, weight=1)
    ## Canvas
    canvas = Canvas(root)
    canvas.grid(row=0, column=0, sticky='nswe')
    ## Frame in canvas
    frm = Frame(canvas)
    ## This puts the frame in the canvas's scrollable zone
    canvas.create_window(0, 0, window=frm, anchor='nw')

    headings=[]  
    for j in range(0,len(DB.mainlist)):   # for each dictionary
        for i in DB.mainlist[j].keys():   # for each key in dictionary
            if i in headings:
                pass
            else:
                headings.append(i)  

    variable = StringVar(frm)
    variable.set(headings[0]) # default value

    w = OptionMenu(frm, variable, *headings).grid(row=0,column=1)
    l = Label(frm, text="Select Search Field").grid(row=0,column=0)
    m = Label(frm, text="Enter Search Term").grid(row=1,column=0)
    v = StringVar()
    e = Entry(frm,relief=RIDGE, textvariable=v)
    e.grid(row=1,column=1)
    go = Button(frm, text='Search', command = lambda : searchGo(variable.get(),e.get()), width=20).grid(row=2, column=0) 

    ## Update display to get correct dimensions
    frm.update_idletasks()
    ## Configure size of canvas's scrollable zone
    canvas.configure(scrollregion=(0, 0, frm.winfo_width(), frm.winfo_height()))

def searchGo(field, entry):
    results = DB.searchforentry(field, entry)
    glist(results)

##################################
# Main Database class that forms the basis of the program

class Database:
    """ This class contains the biblio database. The database stores each entry
        as a dictionary. This class itself contains absolutely no interface code.

        Methods:
                __init__                               - Start up
                addnewentry(newentry)                  - Pass it a dictionary
                searchforentry(searchfield,searchterm) - Pass it two strings, return list of dictionaries
                readfromfile(bibfile)                  - Pass it a file object
                deleteentry(entryquickname)            - Pass it a string
                save(name)                             - Pickle entire DB in file "name"
                load(name)                             - Unpickle entire DB from "name"
                exportasHTML(name)                     - Pass it a filename
                extractentry(entryquickname)           - Return the entry by quickname
          
        Global variables:
                mainlist - A list of dictionaries, each dictionary being a bib entry
                name     - The name of this project/database

	"""
    def __init__(self, name):
        # Create an empty list. This will become a list of dictionaries,
        # where each dictionary contains the data relating to a single
        # BibTeX entry
        self.mainlist = []
        self.name = name

    def addnewentry(self, newentry):
        # This dictionary "newentry" is added to the main list of 
        # dictionaries.
        self.mainlist.append(newentry)

    def searchforentry(self, searchfield,searchterm):
        # Search through all the dictionaries in the main list to find one
        # where 'searchterm' appears in 'searchfield'
        results = []                   # Initialise a new list to store results
        searchterm = searchterm.lower()  # Put the search term into lower case
        for entry in self.mainlist:         # For each entry...
            if entry.has_key(searchfield):  # ...does it have that field? 
                entrystring = entry[searchfield].lower()   # Put the entry into lower case
                                                          # this makes the search case insensitive
                if entrystring.count(searchterm) > 0:  # ...does it match?
                    results.append(entry)   # If yes, add it to the list to be returned
        return results     

    def deleteentry(self, entryquickname):
        # Delete entry with the given quickname
        for j in range(0,len(self.mainlist)):
            if entryquickname != "":
                if "quickname" in self.mainlist[j]:
                    if self.mainlist[j]["quickname"]==entryquickname:
                        return 1
                        del self.mainlist[j]
        return 0

    def extractentry(self,entryquickname):
        # Return entry with the given quickname
        for j in range(0,len(self.mainlist)):
            if entryquickname != "":
                if "quickname" in self.mainlist[j]:
                    if self.mainlist[j]["quickname"]==entryquickname:
                        return self.mainlist[j]
        return 0

    def save(self, name):
        # Pickle the contents of the DB class, to be loaded next time
        library = open(name,'w')
        pickle.dump(self,library)
        library.close()

    def load(self, name):
        # Unpickle the contents of the DB class from a previous time
        try:
            # Load the file, if it exists
            library = open(name,'r')
            self = pickle.load(library)
            # Close the file
            library.close()

        except(IOError), e:
            return 0

    def exportasHTML(self, name):
        # Export the entire database as HTML. Before exporting each key, make sure to check it!
        htmlfile = open(name,'w')
        for j in range(0,len(self.mainlist)):
            if "author" in self.mainlist[j]:
                htmlfile.write(self.mainlist[j]["author"]+"\n")

            if "year" in self.mainlist[j]:
                htmlfile.write(" (" + self.mainlist[j]["year"] + ")" + ": " + "\n" )
 
            if "title" in self.mainlist[j]:
                htmlfile.write("\"" + self.mainlist[j]["title"] + "\","+ "\n" )

            # For articles...
            if "journal" in self.mainlist[j]:
                htmlfile.write("<i> " + self.mainlist[j]["journal"] +"</i> "+ "\n" )
 
            if "volume" in self.mainlist[j]:
                htmlfile.write("<b>" + self.mainlist[j]["volume"] + "</b> "+ "\n" )

            if "pages" in self.mainlist[j]:
                htmlfile.write("p" + self.mainlist[j]["pages"]+ "\n" )

            # For URL - Seems to make ONLY a URL appear for some entries
            #if "howpublished" in self.mainlist[j]:
                #htmlfile.write("\"" + self.mainlist[j]["howpublished"] + "\","+ "\n" )

            htmlfile.write("<br><br>\n\n")
                           

        htmlfile.close()    

    def readfromfile(self, bibfile):
        # Take in the name of a BibTeX file and read all the entries from it
        # bibfile is the name of the file object

        # bibinfo is the temporary dictionary that will be added to the main 
        # list when filled up.
        bibinfo={}
        # ecount is counter of the number of entries read from the current file
        ecount=0
        # line is a misnomer! It contains an entire entry, waiting to be separated
        # into a dictionary and sent to the main list
        line = ""
        
        # Main read loop
        while True:              
             thisline = bibfile.readline()    # Read a single line in  

             # If this is the end of file, we need to save the current entry to the main list
             if thisline=="":
                 # Prep the previous entry for saving
                 line=line.replace("}","")  # Remove any { or } symbols (often used to preserve
                 line=line.replace("{","")  # capitalisation in names
                 line=line.rstrip()        # Strip away and redundant whitespace
                 line=line.lstrip()
                 line = line.split(""",\n""") # Break between fields comes when comma followed immediately by newline
                                              # This prevents breaking accidentally with the commas in a title

                 # The variable 'line' now holds a list of strings. 
                 for i in range(0,len(line)):         # For each one of these
                     newinfo = line[i].split("=")     # Split it into a pair so we have a list of pairs (tuples?)
                     newinfo[0] = newinfo[0].rstrip() # Kill any extra whitespace in both 
                     newinfo[1] = newinfo[1].rstrip()
                     newinfo[0] = newinfo[0].lstrip()
                     newinfo[1] = newinfo[1].lstrip() 
                     newinfo[1] = newinfo[1].replace('\n','')  # Also kill any newline characters or quotemarks
                     newinfo[1] = newinfo[1].replace('\"','')
                     newinfo[1] = newinfo[1].replace('\url','') # Remove \url statements
                     newinfo[0] = newinfo[0].lower() # Make it lower case!
                     bibinfo[newinfo[0]] = newinfo[1]       # Add it to our new temporary dictionary
                 if len(bibinfo.keys()) > 0:
                     self.addnewentry(bibinfo)  # Add the temp dictionary to the main list of dictionaries  
                 break

             # If the line is a comment line, it will start with # or with ! or simply be empty.
             # In this case, ignore it
             elif thisline[0]=="#" or thisline[0]=="!" or thisline=='\n':
                 pass

             # If this line marks the beginning of a new entry, we need to first, save the old entry...
             elif thisline[0] =="@":
                 # save the old entry
                 if line != "":
                     # Prep the previous entry for saving
                     line=line.replace("}","")  # Remove any { or } symbols (often used to preserve
                     line=line.replace("{","")  # capitalisation in names
                     line=line.rstrip()        # Strip away and redundant whitespace
                     line=line.lstrip()
                     line = line.split(""",\n""") # Break between fields comes when comma followed immediately by newline
                                                  # This prevents breaking accidentally with the commas in a title

                     # The variable 'line' now holds a list of strings. 
                     for i in range(0,len(line)):         # For each one of these
                         newinfo = line[i].split("=")     # Split it into a pair so we have a list of pairs (tuples?)
                         newinfo[0] = newinfo[0].rstrip() # Kill any extra whitespace in both 
                         newinfo[1] = newinfo[1].rstrip()
                         newinfo[0] = newinfo[0].lstrip()
                         newinfo[1] = newinfo[1].lstrip() 
                         newinfo[1] = newinfo[1].replace('\n','')  # Also kill any newline characters or quotemarks
                         newinfo[1] = newinfo[1].replace('\"','')
                         newinfo[1] = newinfo[1].replace('\url','') # Remove \url statements
                         newinfo[0] = newinfo[0].lower() # Make it lower case!
                         bibinfo[newinfo[0]] = newinfo[1]       # Add it to our new temporary dictionary
                     if len(bibinfo.keys()) > 0:
                         self.addnewentry(bibinfo)  # Add the temp dictionary to the main list of dictionaries        

                 # Now that we've put the old entry to bed, start reading the new one
                 # First, reset the line variable so it's empty.
                 line = ""      
                 # Add one to the count of entries read
                 ecount=ecount+1
                 # Reset the temporary dictionary
                 bibinfo={}
                 
                 # and get down to work, processing this line first.
                 thisline = thisline.lstrip('@')      # remove the initial @
                 thisline = thisline.rstrip()         # remove whitespace to right
                 thisline = thisline.rstrip(',')      # removes the end comma!
                 # Split the line at the { symbol. Everything to the left becomes the 
                 # 'type' and everything to the right becomes the 'quickname'
                 bibinfo["type"] = thisline.split("{")[0]
                 bibinfo["quickname"] = thisline.split("{")[1] 

             
             # Final case: if this is just another line as part of some entry, append 
             # the present line (thisline) to the string 'line'. It won't be processed 
             # until all the field have been read in.
             else:
                 thisline = thisline.rstrip()                 
                 thisline = thisline.lstrip()              
                 line = line+ " " + thisline + "\n"
             
        # ENDWHILE - End of main loop

        # Return how many entries have been read from the BibTex file
        return ecount


# End of database class!
##########################################################################################


# If the module is being run on its own, as a test, then...
# Note that ALL the interface stuff is here. 
if __name__ == '__main__':
    print
    print ' ----------------------------'
    print ' (B)rilliantly'
    print ' (I)nterfaced'
    print ' (B)ibliography'
    print ' (S)ystem'
    print ' ----------------------------'

    try:
        # Load the Library file, if it exists
        library = open("mainDB.dat", 'r')
        DB = pickle.load(library)
        # Close the Library file
        library.close()

    except(IOError), e:
        print "Library File Not Found", "\n"
        DB = Database("main")

    print 'Press (a) to add new entry manually'
    print '      (t) to fill the Library with test data'
    print '      (r) to read to the Library from a BibTeX file'
    print '      (l) to list Library entries'
    print '      (q) to quit'
    print '      (L) to reload the main Library'
    print '      (S) to save the main Library'
    print '      (H) to export the current library as HTML'
    print '      (d) to delete an entry'
    print '      (np) to create a new project'
    print '      (cp) to close the current project'
    print '      (sp) to save current project'
    print '      (ip) for info on current project'
    print '      (lp) to load a project'
    print '      (ap) to add a project from the Library to the project'
    print '      (ls) to list contents of current project'
    print ' \n----- GUI -----'
    print '      (mm) to bring up main menu'
    print '      (lg) to graphically list main Library'
    print '      (sg) to graphically search main Library'
    print '      (lsg) to graphically list contents of current project'
    while True:
        choice = raw_input('>>')
        
        # Break from the main while loop
        if choice == "q":
            print "Quitting..."
            break;

        # New project
        if choice == "np":
            projname = raw_input("Please enter the name of your new project:")
            CurrentProj = Database(projname)
            CurrentProj.save(projname + ".prj")

        # Close current project
        if choice == "cp":
            CurrentProj = 0
            print 'Closed project!\n'

        # Save current project
        if choice == "sp":
            CurrentProj.save(CurrentProj.name + ".prj")
            print 'Saved project!\n'

        # Load project
        if choice == "lp":
            projfname = raw_input("Please enter the filename of the project to load:")
            try:
                # Load the file, if it exists
                library = open(projfname,'r')
                CurrentProj = pickle.load(library)
                # Close the file
                library.close()
                print 'Loaded project ' + CurrentProj.name

            except(IOError), e:
                print "Unable to load file"

        # Info on current project. Error-catching to see if CurrentProj.name exists at all!
        if choice == "ip":
            try:
                CurrentProj.name
            except:
                print 'No loaded project!'
            else:
                print 'Current project: ' + CurrentProj.name

        # List contents of project
        if choice == "ls":
            print '\n-----------------'
            print 'Project Contents'
            print '-----------------\n'
            for j in range(0,len(CurrentProj.mainlist)):
                print '-----------------'
                for i in CurrentProj.mainlist[j].keys():
                    print i + " : " + CurrentProj.mainlist[j][i]
                print '-----------------\n'

        # Add entry to project
        if choice == "ap":
             eqname = raw_input("Please enter the quickname of the entry in the Library to add to the project:") 
             newentry = DB.extractentry(eqname) 
             if newentry != 0:
                 CurrentProj.addnewentry(newentry)         
       
        # Save the Library File in its current state
        if choice == "S":
            DB.save("mainDB.dat")
            print "Main Library updated \n"

        # Reload the library file from file
        if choice == "L":
            if DB.load("mainDB.dat")==0:
                print "Library File Not Found", "\n"

        # Reload the library file from file
        if choice == "H":
            DB.exportasHTML("lib.html")
            print "Written file lib.html\n"

        # Delete an entry
        if choice == "d":
            qname = raw_input("Please enter the quickname of the entry to delete:")
            if DB.deleteentry(qname)==1:
                print "Entry deleted"
            else:
                print "No entry found to delete"

        # Put some test data in there, for debugging
        if choice == "t":
            # test data
            a = {"author" : "edd", "year" : "2009"}
            DB.addnewentry(a)
            a = {"author" : "jim", "year" : "2009"}
            DB.addnewentry(a)
            print 'Filled with test data!'

        # Read from a file.
        if choice == "r":
            f = raw_input("Please enter the name of the file, including the full path:")

            try:
                bibf = open(f, 'r')
                count = DB.readfromfile(bibf)
                print 'Read %i entries' % count
                # Close the BibTex file
                bibf.close()

            except(IOError), e:
                print "Unable to open the file", f, "\n"

        # Manually a new entry.
        if choice == "a":
            bibinfo = {}
      	    print 'Leave field blank to finish' 
            field = "not cake"
		    
            while True:
                field = raw_input('Enter field: ')

                if field == "":
                    break;

                value = raw_input('Enter value: ')
                bibinfo[field] = value

            if (len(bibinfo) != 0):
                DB.addnewentry(bibinfo)
                print 'Added new entry'

        # List the current entries
        if choice == "l":
            print '\n-----------------'
            print 'Library Contents'
            print '-----------------\n'
            for j in range(0,len(DB.mainlist)):
                print '-----------------'
                for i in DB.mainlist[j].keys():
                    print i + " : " + DB.mainlist[j][i]
                print '-----------------\n'

        # List the current entries, graphically. 
        if choice == "lg":
            glist(DB.mainlist)


        # Search for entries
        if choice=="s":
            print '\n Search Mode\n'
            field = raw_input("Enter field: ")
            term = raw_input("Enter term of part of term to match: ")
            results = DB.searchforentry(field,term)
            for j in range(0,len(results)):
                print '-----------------'
                for i in results[j].keys():
                    print i + " : " + results[j][i]
                print '-----------------\n'          
            print 'Found %i matches' % len(results)
            print '-----------------\n'  

        # Search for entries, displaying results graphically   
        if choice=="sg":
            print '\n Graphical Search Mode\n'
            field = raw_input("Enter field: ")
            term = raw_input("Enter term of part of term to match: ")
            results = DB.searchforentry(field,term)
            glist(results)
            print 'Found %i matches' % len(results)
            print '-----------------\n'  

        # Graphically List contents of project
        if choice == "lsg":
            try:
                glist(CurrentProj.mainlist)
            except:
                print 'No current project!'

        # Bring up the GUI main menu
        if choice == "mm":
            MainMenu()

    # ENDWHILE - End of main loop
    # Quit
    exit()
##########################################################################################
